package se.slackers.floatingtableheader.client;

import se.slackers.floatingtableheader.client.resources.Resources;

import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ScrollEvent;
import com.google.gwt.user.client.Window.ScrollHandler;
import com.google.gwt.user.client.ui.RootPanel;

/**
 *
 * @author bysse
 */
public class FloatingHeader implements ScrollHandler, ResizeHandler {
	private static final int INITIALIZATION_DELAY = 250;

	private final Element element;
	private final HeaderGenerator columnGenerator;
	private final HeaderGenerator rowGenerator;

	private Element rowHeader;
	private Element columnHeader;

	private int tableOffsetX;
	private int tableOffsetY;
	private int tableWidth;
	private int tableHeight;

	private boolean rowHeaderVisible;
	private boolean colHeaderVisible;

	private HandlerRegistration resizeHandler;
	private HandlerRegistration scrollHandler;

	/**
	 * Creates a FloatingHeader.
	 *
	 * A typical way to instantiate this class is to use {@link FloatingHeaderBuilder} as shown
	 * below.
	 *
	 * <pre>
	 * FloatingHeaderBuilder.from(RootPanel.get(&quot;t1&quot;)).column().byClass(&quot;header&quot;).build();
	 * </pre>
	 *
	 * @param element
	 * @param columnGenerator
	 * @param rowGenerator
	 */
	public FloatingHeader(final Element element, final HeaderGenerator columnGenerator, final HeaderGenerator rowGenerator) {
		this.element = element;
		this.columnGenerator = columnGenerator;
		this.rowGenerator = rowGenerator;

		resizeHandler = Window.addResizeHandler(this);
		scrollHandler = Window.addWindowScrollHandler(this);


	}

	/**
	 * Create and attach the floating header to the DOM using delayed initialization strategy. This
	 * method can be called any time to re-initialize the headers.
	 */
	public void attach() {
		detach();
		attach(Strategy.DELAYED);
	}

	/**
	 * Create and attach the floating header to the DOM. This method can be called any time to
	 * re-initialize the headers.
	 *
	 * @param mode
	 *            What strategy to use when initializing the headers
	 */
	public void attach(final Strategy strategy) {
		switch (strategy) {
		default:
		case DELAYED:
			new Timer() {
				@Override
				public void run() {
					create();
				}
			}.schedule(INITIALIZATION_DELAY);
			break;
		case INSTANT:
			create();
		}
	}

	/**
	 * Detach the floating header from the DOM.
	 */
	private void detach() {
		// remove the old headers
		if (rowHeader != null) {
			rowHeader.removeFromParent();
			rowHeader = null;
		}

		if (columnHeader != null) {
			columnHeader.removeFromParent();
			columnHeader = null;
		}
	}

	/**
	 * Create the headers.
	 */
	private void create() {
		calculateTableDimensions();
		buildFloatingTableHeaders();
	}

	/**
	 * Remove all handlers and the created floating header tables.
	 */
	public void cleanup() {
		resizeHandler.removeHandler();
		scrollHandler.removeHandler();

		detach();
	}

	@Override
	public void onResize(final ResizeEvent event) {
		if (rowHeader != null || columnHeader != null) {
			// re-create the headers
			create();
		}
	}

	@Override
	public void onWindowScroll(final ScrollEvent event) {
		updateHeaders();
	}

	/**
	 * Updates the position and takes care of showing hiding the headers.
	 */
	private void updateHeaders() {
		if (rowHeader != null) {
			if (rowHeaderVisible) {
				if (isRowHeaderVisible(rowHeader)) {
					// update the header position
					updateRowHeaderPosition(rowHeader);
				} else {
					// hide the header
					hide(rowHeader);
					rowHeaderVisible = false;
				}
			} else if (isRowHeaderVisible(rowHeader)) {
				// show the header
				updateRowHeaderPosition(rowHeader);
				show(rowHeader);
				rowHeaderVisible = true;
			}
		}

		if (columnHeader != null) {
			if (colHeaderVisible) {
				if (isColumnHeaderVisible(columnHeader)) {
					// update the header position
					updateColumnHeaderPosition(columnHeader);
				} else {
					// hide the header
					hide(columnHeader);
					colHeaderVisible = false;
				}
			} else if (isColumnHeaderVisible(columnHeader)) {
				// show the header
				updateColumnHeaderPosition(columnHeader);
				show(columnHeader);
				colHeaderVisible = true;
			}
		}
	}

	/**
	 * Update the position of the column header.
	 *
	 * @param header
	 */
	private final void updateColumnHeaderPosition(final Element header) {
		final int left = tableOffsetX - Window.getScrollLeft();
		final Style style = header.getStyle();
		style.setTop(0, Unit.PX);
		style.setLeft(left, Unit.PX);
	}

	/**
	 * Update the position of the row header.
	 *
	 * @param header
	 */
	private final void updateRowHeaderPosition(final Element header) {
		final int top = tableOffsetY - Window.getScrollTop();
		final Style style = header.getStyle();
		style.setTop(top, Unit.PX);
		style.setLeft(0, Unit.PX);
	}

	/**
	 * Create cloned versions of the table and attach them to BODY.
	 */
	private void buildFloatingTableHeaders() {
		if (rowGenerator != null) {
			rowHeader = rowGenerator.create(element);
			rowHeader.addClassName(Resources.use.css().floatClass());
			rowHeaderVisible = isRowHeaderVisible(rowHeader);
			if (!rowHeaderVisible) {
				hide(rowHeader);
			}
			RootPanel.getBodyElement().appendChild(rowHeader);
		}

		if (columnGenerator != null) {
			columnHeader = columnGenerator.create(element);
			columnHeader.addClassName(Resources.use.css().floatClass());
			colHeaderVisible = isColumnHeaderVisible(columnHeader);
			if (!colHeaderVisible) {
				hide(columnHeader);
			}
			RootPanel.getBodyElement().appendChild(columnHeader);
		}

		updateHeaders();
	}

	/**
	 * Store the tables dimensions for later use.
	 */
	private void calculateTableDimensions() {
		tableOffsetX = element.getAbsoluteLeft();
		tableOffsetY = element.getAbsoluteTop();
		tableWidth = element.getOffsetWidth();
		tableHeight = element.getOffsetHeight();
	}

	/**
	 * Returns true is the column header should be visible.
	 *
	 * @param header
	 * @return
	 */
	private boolean isColumnHeaderVisible(final Element header) {
		final int windowY = Window.getScrollTop();
		return tableOffsetY < windowY && windowY < tableOffsetY + tableHeight;
	}

	/**
	 * Returns true if the row header should be visible.
	 *
	 * @param header
	 * @return
	 */
	private boolean isRowHeaderVisible(final Element header) {
		final int windowX = Window.getScrollLeft();
		return tableOffsetX < windowX && windowX < tableOffsetX + tableWidth;
	}

	/**
	 * Hides an element.
	 *
	 * @param element
	 */
	private void hide(final Element element) {
		element.addClassName(Resources.use.css().hideClass());
	}

	/**
	 * Shows an element.
	 *
	 * @param element
	 */
	private void show(final Element element) {
		element.removeClassName(Resources.use.css().hideClass());
	}
}